import "Turbine";
import "Turbine.Gameplay";
import "Turbine.UI";
import "Turbine.UI.Extensions";
import "Turbine.UI.Lotro";
import "Turbine.Utils";

--Turbine.Shell.WriteLine ("debug");

-- Sorting function for structured associative arrays (indexed dictionaries), sorted by second value which needs to be numeric
-- Zero values sort are returned last
function dictionarySort(a,b)
	if a[2] == 0 then return false end
	if b[2] == 0 then return true end
	return a[2] < b[2]
end


-- Main function, entry point
LotroLCDWindow = class(Turbine.UI.Window);
function LotroLCDWindow:Constructor()
	Turbine.UI.Lotro.Window.Constructor(self);
	
	--Create player object and define combat trigger functions
	self:CombatTriggers(self);
	
	--Load saved settings from file or initalize settings with default values
	self.settings = self:LoadSettings(self);
	
	-- Get one update per frame, triggers LotroLCD:Update
	self:SetWantsUpdates(true);
	
	-- Set Look, feel, size  and position
	self:SetBackColorBlendMode(Turbine.UI.BlendMode.Normal);
	self:SetOpacity(self.settings.opacity);
	self:SetText("LotroLCD");
	self:SetPosition(self.settings.x, self.settings.y);
	self:SetSize(165, 100);
	self:SetVisible(true);
	
	-- Update counter, used to track intervals between updating plugin screen 
	self.updateCounter = 0;
	
	-- Initialize Last Combat Time
	self.lastCombatTime = Turbine.Engine.GetGameTime();
	
	-- Create the Version Label
	self:ConstructVersionLabel(self);
	
	-- Create placeholder for minimize/hide functionality
	self:PlaceholderConstructor(self);
	
	-- Create Config Window
	self:ConfigWindowConstructor(self);
	
	-- Create Config Button
	self:ConstructConfigButton(self, self.configWindow);
	
	-- Create Message Window
	--self:MessageWindowConstructor(self);
	
	-- Detect double clicks on title to minimize/hide
	self.MouseDoubleClick = function( sender, args )
		self:SetVisible(false);
		self.placeholder:SetVisible(true);
	end
	
	-- Listbox to automatically layout multiple widgets as a group, keys
	self.keyListbox = Turbine.UI.ListBox();
	self.keyListbox:SetParent(self);
	self.keyListbox:SetPosition(30, 50);
	self.keyListbox:SetSize(60, 20);
	
	-- Filter list of supported elements, get enabled elements sorted
	self.enabledElements = self:GetEnabledElements(self, self.settings.dataElements);
	
	-- Retrive values for each element
	self.enabledElementValues = self:GetValues(self,self.enabledElements);
	
	-- Add label/button to stats window for each enabled data element
	self:AdjustLabels(self,self.enabledElements,self, self.keyListbox);
	
	-- Listbox to automatically layout multiple widgets as a group, values
	self.valueListbox = Turbine.UI.ListBox();
	self.valueListbox:SetParent(self);
	self.valueListbox:SetPosition(90, 50);
	self.valueListbox:SetSize(40, 20);
	
	-- Add label/button to stats window for each enabled data element
	self:AdjustLabels(self,self.enabledElementValues,self,self.valueListbox);
end



--Create player object and define combat trigger functions
function LotroLCD:CombatTriggers(self)
	-- Get current player, for Nov 2010 upgrade
	if pcall( Turbine.Gameplay.LocalPlayer.GetInstance ) then
		self.player = Turbine.Gameplay.LocalPlayer.GetInstance();
	else
		self.player = Turbine.Gameplay.LocalPlayer();
	end

	-- Function to detect when combat state changes and perform relevant actions
	self.player.InCombatChanged = function( sender, args )
		self.lastCombatTime = Turbine.Engine.GetGameTime(); -- Ensure combat file is parsed for a grace period after combat starts and ends
		if self.settings.show == 0 then
			if self.player:IsInCombat() then
				self:SetVisible(false);
			else
				self:SetVisible(true);
			end
		elseif self.settings.show == 3 then
			if self.player:IsInCombat() then
				self:SetVisible(true);
			else
				self:SetVisible(false);
			end
		end
	end

	-- Workaround because AoE combats where mobs don't hit you don't cause you to go into combat state
	self.player.PowerChanged = function( sender, args )
		-- Trigger parsing of combat file if power changes, workaround
		self.lastCombatTime = Turbine.Engine.GetGameTime();
	end
end

-- Create a config button
function LotroLCDWindow:ConstructConfigButton(self, window)
	self.configButton = Turbine.UI.Button();
	self.configButton:SetParent( self );
	self.configButton:SetPosition( 5, 25);
	self.configButton:SetBackground("LotroLCD/LotroLCD/Resources/LotroLCDSettings.tga");
	self.configButton:SetSize(20, 20);
	self.configButton:SetBlendMode(Turbine.UI.BlendMode.AlphaBlend)
	self.configButton.MouseClick = function( sender, args )
		if window:IsVisible() then
			window:SetVisible(false);
		else
			window:SetVisible(true);
		end
	end	

	-- On entering config button show different graphics
	self.configButton.MouseEnter = function( sender, args )
		self.configButton:SetBackground("LotroLCD/LotroLCD/Resources/LotroLCDSettingsMouseOver.tga");
	end

	-- On leaving config button show original graphics
	self.configButton.MouseLeave = function( sender, args )
		self.configButton:SetBackground("LotroLCD/LotroLCD/Resources/LotroLCDSettings.tga");
	end
end


-- Create a version label shown in lower right corner of "self" window
function LotroLCDWindow:ConstructVersionLabel(self)
	self.version = Turbine.UI.Label();
	self.version:SetParent(self);
	self.version:SetPosition(120, 85);
	self.version:SetSize(50, 50);
	self.version:SetFontStyle(Turbine.UI.FontStyle.Outline);
	self.version:SetFont(Turbine.UI.Lotro.Font.Verdana10);
	self.version:SetTextAlignment(Turbine.UI.ContentAlignment.MiddleLeft);
	self.version:SetMouseVisible(false);
	local loaded_plugins = Turbine.PluginManager.GetAvailablePlugins()
	for i,v in ipairs(loaded_plugins) do
		if v.Name == "LotroLCD" then
			Turbine.Shell.WriteLine (string.format("%s v%s",v.Name,v.Version));
			self.version:SetText(v.Version);
		end
	end
end

-- A placeholder to show when a window is hidden or closed
function LotroLCD:PlaceholderConstructor(self)
	self.placeholder = Turbine.UI.Window( );
	self.placeholder:SetVisible(false);
	self.placeholder:SetOpacity(self.settings.opacity);
	self.placeholder:SetText("LotroLCD");
	self.placeholder:SetPosition(self.settings.x + 29, self.settings.y + 3 );
	self.placeholder:SetSize(107, 29);
	self.phbutton = Turbine.UI.Button();
	self.phbutton:SetParent( self.placeholder );
	self.phbutton:SetPosition( 0, 0 );
	self.phbutton:SetSize( 107, 29 );
	self.phbutton:SetBackground("LotroLCD/LotroLCD/Resources/LotroLCDButton.tga");
	self.phbutton.MouseClick = function( sender, args )
		self:SetVisible(true);
		self.placeholder:SetVisible(false);
	end
end

-- Create a Config Window
function LotroLCD:ConfigWindowConstructor(self)
	self.configWindow = Turbine.UI.Lotro.Window();
	
	-- Set Look, feel and size
	self.configWindow:SetVisible(false);
	self.configWindow:SetBackColorBlendMode(Turbine.UI.BlendMode.Normal);
	self.configWindow:SetOpacity(settings.opacity);
	self.configWindow:SetText("LotroLCD Settings");
	self.configWindow:SetSize(300, 200);
	
	-- Automatically center the settings window
	local displayWidth, displayHeight = Turbine.UI.Display.GetSize();
    local windowWidth, windowHeight = self.configWindow:GetSize();  
    self.configWindow:SetPosition( ( displayWidth - windowWidth ) / 2, 100 );
	
	local genericSettingsLabel = Turbine.UI.Label();
	genericSettingsLabel:SetPosition( 30, 40 );
	genericSettingsLabel:SetSize( 200, 25 );
	genericSettingsLabel:SetFontStyle(Turbine.UI.FontStyle.Outline);
	genericSettingsLabel:SetFont(Turbine.UI.Lotro.Font.Verdana18);
	genericSettingsLabel:SetTextAlignment(Turbine.UI.ContentAlignment.TopLeft);
	genericSettingsLabel:SetText("Generic Settings");
	genericSettingsLabel:SetParent(self.configWindow);
	
	local opacityLabel = Turbine.UI.Label();
	opacityLabel:SetPosition( 50, 70 );
	opacityLabel:SetSize( 100, 25 );
	opacityLabel:SetFontStyle(Turbine.UI.FontStyle.Outline);
	opacityLabel:SetFont(Turbine.UI.Lotro.Font.Verdana14);
	opacityLabel:SetTextAlignment(Turbine.UI.ContentAlignment.TopLeft);
	opacityLabel:SetText("Opacity");
	opacityLabel:SetParent(self.configWindow);
	
	-- Opacity slider
	local opacitySlider =  Turbine.UI.Lotro.ScrollBar(); 
	opacitySlider:SetPosition( 150, 75 );
	opacitySlider:SetBackColor( Turbine.UI.Color( 0.1, 0.1, 0.1 ) );
	opacitySlider:SetSize( 100, 10 );
	opacitySlider:SetParent(self.configWindow);
	opacitySlider:SetValue(settings.opacity * 100);
	
	opacitySlider.ValueChanged = function( sender, args )
		local opacity = opacitySlider:GetValue();
		settings.opacity = opacity / 100;
		self:SetOpacity(self.settings.opacity);
		self.placeholder:SetOpacity(settings.opacity);
		self.configWindow:SetOpacity(settings.opacity);
		self:SaveSettings(self);
	end
	
	local dataElementsLabel = Turbine.UI.Label();
	dataElementsLabel:SetPosition( 30, 100 );
	dataElementsLabel:SetSize( 200, 25 );
	dataElementsLabel:SetFontStyle(Turbine.UI.FontStyle.Outline);
	dataElementsLabel:SetFont(Turbine.UI.Lotro.Font.Verdana18);
	dataElementsLabel:SetTextAlignment(Turbine.UI.ContentAlignment.TopLeft);
	dataElementsLabel:SetText("Data Elements");
	dataElementsLabel:SetParent(self.configWindow);
	
	-- Listbox to automatically layout multiple widgets as a group
	self.configWindow.listbox = Turbine.UI.ListBox();
	self.configWindow.listbox:SetParent( self.configWindow );
	self.configWindow.listbox:SetPosition( 50, 130 );
	self.configWindow.listbox:SetSize( 120, 100 );
	
	-- Add label/button to config window for each supported data element
	self.configWindow.sortedDataElements = self:SortElements(settings.dataElements);
	self:AddButtons(self,self.configWindow.sortedDataElements,self.configWindow, self.configWindow.listbox)
	
	return self.configWindow;
end

-- Create a Message Window
function LotroLCD:MessageWindowConstructor(self)
	self.messageWindow = Turbine.UI.Lotro.Window();
	
	-- Set Look, feel and size
	self.messageWindow:SetVisible(true);
	self.messageWindow:SetBackColorBlendMode(Turbine.UI.BlendMode.Normal);
	self.messageWindow:SetOpacity(settings.opacity);
	self.messageWindow:SetText("Messages");
	self.messageWindow:SetSize(300, 200);
	
	-- Automatically center the settings window
	local displayWidth, displayHeight = Turbine.UI.Display.GetSize();
    local windowWidth, windowHeight = self.messageWindow:GetSize();  
    self.messageWindow:SetPosition( ( displayWidth - windowWidth ) / 2, ( displayHeight - windowHeight ) / 2 );
	
	-- Listbox to automatically layout multiple widgets as a group, messages
	self.messageListbox = Turbine.UI.ListBox();
	self.messageListbox:SetParent(self.messageWindow);
	self.messageListbox:SetPosition(30, 50);
	self.messageListbox:SetSize(100, 20);
	
	-- Add label/button to stats window for each enabled message
	self:AdjustLabels(self,settings.messages,self.messageWindow,self.messageListbox);
	
	
	return self.messageWindow;
end	

-- Retrieve Values for elemements supplied in an array
function LotroLCD:GetValues(self,elements)
	local loadedValues = {}; -- A map of retrieved values
	local sortedValues = {};
	for i,v in ipairs(elements) do -- For all elements try fetching values
		loadedValues[v] = self:GetValue(string.format("%sValue",v));
	end
	for i,v in ipairs(elements) do -- Return a simple array with void fields as "---"
		if loadedValues[v] then
			table.insert(sortedValues,loadedValues[v]);
		else
			table.insert(sortedValues,"---");
		end
	end
	return sortedValues;
end

-- Function used to load data from the command line (which was put there by LoadData plugin)
function LotroLCD:GetValue(searchPattern)
	local commands = Turbine.Shell.GetCommands();
	local value;
	for k, v in pairs(commands) do
		if string.find(v, searchPattern) == 1 then
			value = string.sub(v, string.len(searchPattern)+1);
		end
	end
	return value;
end

-- Add a push button to a listbox, resizing it and it's parent window
function LotroLCDWindow:AddButtons(self,elements,window, listbox)
	for i,v in ipairs(elements) do
		local width, height = listbox:GetSize();
		-- Create new button for each element
		local listItem = Turbine.UI.Lotro.Button();
		
		-- Set Look, feel, size  and position
		listItem:SetSize( 100, 20 );
		listItem:SetFontStyle(Turbine.UI.FontStyle.Outline);
		listItem:SetFont(Turbine.UI.Lotro.Font.Verdana14);
		listItem:SetTextAlignment(Turbine.UI.ContentAlignment.MiddleCenter);
		listItem:SetText(v[1]);
		
		if v[2] > 0 then -- If element is visible
			listItem:SetBackColor( Turbine.UI.Color( 0, 0.2, 0 ) );
		else
			listItem:SetBackColor( Turbine.UI.Color( 0.2, 0, 0 ) );
		end
		
		-- Add to window listbox and scale master window
		listbox:AddItem( listItem );
		window:SetHeight(height + 80);
		listbox:SetHeight( height + 20);
		
		listItem.MouseClick = function( sender, args )
			if elements[i][2] > 0 then
				elements[i][2] = 0;
			else
				elements[i][2] = listbox:IndexOfItem(listItem);
			end
			
			-- Assign new position value to dictionary based on array
			self:RepositionElements(elements)
			
			-- Convert config settings to plane dictionary and save
			self.settings.dataElements = self:ArrayOfDictToDict(elements);
			self:SaveSettings(self);
			
			-- Resort config elements with new values
			elements = self:SortElements(self.settings.dataElements);
			
			-- Create a new list of enabled elements for status window
			self.enabledElements = self:GetEnabledElements(self, self.settings.dataElements);
			
			-- Fetch values for new elements
			self.enabledElementValues = self:GetValues(self,self.enabledElements);

			-- Rebuild the listbox in the config window
			self:RebuildListBox(elements,listbox);
			
			-- Rebuild the listbox in the stats window
			self:RebuildListBox(self.enabledElements,self.keyListbox);
		end
	end
end

-- Rebuilds list boxes from new a new array of dictionaries or sorted array
function LotroLCDWindow:RebuildListBox(elements,listbox)
	for i,v in ipairs(elements) do
		local key;
		local value;
		local currentItem = listbox:GetItem(i);
		--Handle input as array of dictionaries or sorted array
		if type(v) == "table" then
			value = v[1];
			key = v[2];
			
			-- If item is in an array of dictionaries and is visible
			if key > 0 then
				currentItem:SetBackColor( Turbine.UI.Color( 0, 0.2, 0 ) );
			else
				currentItem:SetBackColor( Turbine.UI.Color( 0.2, 0, 0 ) );
			end
		else
			value = v;
			key = i;
		end
		currentItem:SetText(value);
	end

	-- Add labels for extra elements as required
	self:AdjustLabels(self,self.enabledElements,self, self.keyListbox);
	self:AdjustLabels(self,self.enabledElementValues,self,self.valueListbox);
end

--Reassign positional values in key, position dictionary based on encapsulating array
function LotroLCD:RepositionElements(elements)
	for i,v in ipairs(elements) do
		if v[2] > 0 then
			elements[i][2] = i;
		end
	end
end

-- Add pushs button to a listbox as required for all elements, resizing it and parent window as needed
function LotroLCDWindow:AdjustLabels(self,elements,window, listbox)
	--Add Labels as required
	for i,v in ipairs(elements) do
		if i > listbox:GetItemCount() then
			local width, height = listbox:GetSize();
			-- Create new button for each element
			local listItem = Turbine.UI.Label();
			
			-- Set Look, feel, size  and position
			listItem:SetSize( width, 20 );
			listItem:SetFontStyle(Turbine.UI.FontStyle.Outline);
			listItem:SetFont(Turbine.UI.Lotro.Font.Verdana14);
			listItem:SetTextAlignment(Turbine.UI.ContentAlignment.MiddleLeft);
			listItem:SetMultiline( false );
			listItem:SetZOrder(100);
			listItem:SetText(v);
			
			-- Add to window listbox and scale master window
			listbox:AddItem( listItem );
			window:SetHeight(height + 70);
			listbox:SetHeight( height + 20);
		end
	end
	--Remove Labels as required
	local elementBalance = listbox:GetItemCount() - #elements ;
	for n = 1,elementBalance do
		local width, height = listbox:GetSize();
		listbox:RemoveItemAt(listbox:GetItemCount());
		window:SetHeight(height + 30);
		listbox:SetHeight( height - 20);
	end
	
	-- Should probably be somewhere else, but easy to have here for now
	local width, height = listbox:GetSize();
	self.version:SetPosition( 120, height + 15);
end

-- Takes a dictionary with positions as value, returns sorted array by positions.
function LotroLCD:GetEnabledElements(self,elements)
	local enabledDataElements = {};
	
	-- Create an array of dictionaries so it can be sorted
	for k,v in pairs(elements) do -- For each Data element
		if v > 0 then --If value is supposed to be visible
			enabledDataElements[k] = v;
		end
	end
	
	-- Sort Dictionaries by value
	local sortedArrayOfDictionaries = self:SortElements(enabledDataElements);
	
	-- Convert to Array of Elements and return
	return self:ArrayOfDictToArray(sortedArrayOfDictionaries);
end

-- Create a dictionary from an array of dictionaries
function LotroLCD:ArrayOfDictToDict(array)
	local simpleDictionary  = {};
	for i,v in ipairs(array) do
		simpleDictionary[v[1]] = v[2];
	end
	return simpleDictionary;
end

-- Create simple sorted array from array of dictionaries
function LotroLCD:ArrayOfDictToArray(array)
	local sortedArray  = {};
	
	for i,v in ipairs(array) do
		table.insert(sortedArray,v[1]);
	end
	
	return sortedArray;
end

-- Sort a Dictionary by it's values, return array in order specified in value
function LotroLCD:SortElements(elements)
	local arrayOfDictionaries = {};
	
	-- Convert dictionary to array of dictionaries
	for k,v in pairs(elements) do 
		table.insert(arrayOfDictionaries,{k,v});
	end
	
	-- Sort using dictionarySort function
	table.sort(arrayOfDictionaries, dictionarySort)
	
	return arrayOfDictionaries;
end

-- Handle Updates
function LotroLCD:Update()
	if self.updateCounter == 10 then -- Only update plugin every Nth time a frame is drawn
		if self:IsVisible() then
			if self.player:IsInCombat() or self.lastCombatTime > (Turbine.Engine.GetGameTime() - 4) then -- 4 Second grace period, for logs to catch up and make sure we show entire combat
				Turbine.PluginManager.LoadPlugin("LoadData");
				for i,v in pairs(self.enabledElements) do
					local value = self:GetValue(string.format("%sValue", v));
					local currentItem = self.valueListbox:GetItem(i);
					if value then
						currentItem:SetText(string.format("%s", value));
					else
						currentItem:SetText("---");
					end
				end
				Turbine.PluginManager.UnloadScriptState("LoadData");
			end
		end
		self.updateCounter = 0;
	end
	self.updateCounter = self.updateCounter + 1; -- Increment UpdateCounter
end

-- Handle changes in visibility when triggered by user
function LotroLCD:VisibleChanged(sender, args)
	if self:IsVisible() then
		self.placeholder:SetVisible(false);
	else
		self.placeholder:SetVisible(true);
	end
end

-- Handle user moving window around
function LotroLCD:PositionChanged(sender, args)
	local x, y = self:GetPosition();
	self.placeholder:SetPosition(x + 29, y + 3);
	self.settings.x = x;
	self.settings.y = y;
	self:SaveSettings(self);
end

-- Save current settings
function LotroLCD:SaveSettings(self)
	Turbine.PluginData.Save(Turbine.DataScope.Character, "LotroLCD", self.settings);
end

-- Load settings from file, or initialize with default values
function LotroLCD:LoadSettings(self)

	-- Touch file to indicate plugin is loaded so C code can find where to put data
	Turbine.PluginData.Save(Turbine.DataScope.Account, "LoadData", {touch = true}); 
	
	-- Get display and window size, used for calculating default initial values
	local displayWidth, displayHeight = Turbine.UI.Display.GetSize();
    local windowWidth, windowHeight = self:GetSize();
	
	settings = Turbine.PluginData.Load(Turbine.DataScope.Character, "LotroLCD");
	if type(settings) ~= "table" then
		settings = { };
	end
	settings.defaultElements = {	ctime = 1,
									dmg = 2,
									dps = 3,
									hdps = 4,
									tdmg = 5,
									tdps = 6,
									heal = 7,
									hps = 8,
									pdmg = 9,
									pdps = 10,
									spdmg = 11,
									spdps = 12,
									adwarfdmg = 0,
									beldmg = 0,
									westdmg = 0,
									commondmg = 0,
									firedmg = 0,
									lightdmg = 0,
									lightningdmg = 0,
									frostdmg = 0,
									shadowdmg = 0,
									aciddmg = 0,
									tadwarfdmg = 0,
									tbeldmg = 0,
									twestdmg = 0,
									tcommondmg = 0,
									tfiredmg = 0,
									tlightdmg = 0,
									tlightningdmg = 0,
									tfrostdmg = 0,
									tshadowdmg = 0,
									taciddmg = 0}
	if not settings.dataElements then
		settings.dataElements = settings.defaultElements;
	end
	
	settings.messages = {	Message1 = 1,
							Message2 = 2,
							Message3 = 3,
							Message4 = 4,
							Message5 = 5}
	
	-- Make sure all available data elements are represented in the users configuration
	for k,v in pairs(settings.defaultElements) do
		if not settings.dataElements[k] then
			settings.dataElements[k] = v;
			Turbine.Shell.WriteLine (string.format("New data element available: %s",k));
		end
	end
	if not settings.x then
		settings.x = ( displayWidth - windowWidth ) / 2;
	end
	if not settings.y then
		settings.y = ( displayHeight - windowHeight ) / 2;
	end
	if not settings.opacity then
		settings.opacity = 0.8;
	end
	if not settings.show then
		settings.show = 1;
	end
	return settings;
end

--Run LotroLCD
lotroLCD = LotroLCD();

-- Command Line Interface
LotroLCDCommandLineInterface = class(Turbine.ShellCommand);
Turbine.Shell.AddCommand('lotrolcd', lotroLCDCommandLineInterface);
function LotroLCDCommandLineInterface:Execute(command, arguments)
	if arguments == "post" then
		self.settings.show = 0;
		self:SaveSettings(self);
		if self.player:IsInCombat() then
			self:SetVisible(false);
		else
			self:SetVisible(true);
		end
	elseif arguments == "hide" then
		self.settings.show = 2;
		self:SaveSettings(self);
		self:SetVisible(false);
	elseif arguments == "show" then
		self.settings.show = 1;
		self:SaveSettings(self);
		self:SetVisible(true);
	elseif arguments == "during" then
		self.settings.show = 3;
		self:SaveSettings(self);
		if self.player:IsInCombat() then
			self:SetVisible(true);
		else
			self:SetVisible(false);
		end
	else
		Turbine.Shell.WriteLine("Usage: /lotrolcd <during|post|show|hide>");
	end
end

-- Help function, shows command line help
function LotroLCDCommandLineInterface:GetShortHelp()
	return "usage: /lotrolcd <during|post|show|hide>";
end

lotroLCDCommandLineInterface = LotroLCDCommandLineInterface();